package com.lemon.exam.service.impl;

import com.alibaba.fastjson2.JSON;
import com.lemon.exam.common.constant.ApiConst;
import com.lemon.exam.common.crud.BaseServiceImpl;
import com.lemon.exam.common.exception.CustomException;
import com.lemon.exam.common.properties.ConfigProperties;
import com.lemon.exam.common.redis.RedisService;
import com.lemon.exam.common.util.PaperUtil;
import com.lemon.exam.common.util.SecurityUtil;
import com.lemon.exam.common.web.Result;
import com.lemon.exam.entity.dto.PaperAnswerDTO;
import com.lemon.exam.entity.po.PaperAnswerPO;
import com.lemon.exam.entity.po.PaperPO;
import com.lemon.exam.entity.vo.PaperAnswerVO;
import com.lemon.exam.entity.vo.PaperQuestionVO;
import com.lemon.exam.entity.vo.PaperVO;
import com.lemon.exam.repository.PaperAnswerRepository;
import com.lemon.exam.service.IPaperAnswerService;
import com.lemon.exam.service.IPaperService;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.RateLimiterRegistry;
import io.github.resilience4j.reactor.ratelimiter.operator.RateLimiterOperator;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.data.relational.core.query.Update;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.util.retry.Retry;
import reactor.util.retry.RetryBackoffSpec;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

import static com.lemon.exam.common.enums.CodeEnum.ERROR_402;

/**
 * 试卷-答案表 服务实现类
 *
 * @author Lemon
 * @since 2025/05/17 10:51:17
 */
@Slf4j
@Service
public class PaperAnswerServiceImpl extends BaseServiceImpl<PaperAnswerPO, Long, PaperAnswerRepository> implements IPaperAnswerService {
    //交卷重试配置
    private static final RetryBackoffSpec RETRY_SPEC = Retry
            .backoff(3, Duration.ofMillis(100))
            .maxBackoff(Duration.ofSeconds(1))
            .jitter(0.5)
            .doBeforeRetry(s -> log.warn("正在重试交卷（尝试{}）", s.totalRetries()));

    //限流配置
    private static final RateLimiterConfig RATE_LIMITER_CONFIG = RateLimiterConfig.custom()
            .limitForPeriod(1000) // 每秒1000个请求
            .limitRefreshPeriod(Duration.ofSeconds(1))
            .timeoutDuration(Duration.ofMillis(100)) // 等待超时时间
            .build();

    //交卷配置限流器
    private final RateLimiter RATE_LIMITER = RateLimiterRegistry.of(RATE_LIMITER_CONFIG).rateLimiter("paperAnswerRateLimiter");

    @Resource
    private IPaperService paperService;
    @Resource
    private ConfigProperties configProperties;
    @Resource
    private RedisService redisService;

    @Override
    public Flux<PaperVO> findPaperList(String type, int pageNum, int pageSize) {
        return switch (type) {
            case "padding" -> paperService.findPaddingPaperByAnswerUser(pageNum, pageSize);
            case "finish" -> paperService.findFinishPaperByAnswerUser(pageNum, pageSize);
            case "notFill" -> paperService.findNotFillPaperByAnswerUser(pageNum, pageSize);
            default -> throw new CustomException(ERROR_402);
        };
    }

    @Override
    public Mono<Result<PaperAnswerVO>> getPaperAnswer(Long paperId) {
        return paperService.findPaperById(paperId).flatMap(paper -> {
            if (LocalDateTime.now().isBefore(paper.getStartTime())) {
                return Mono.just(Result.err(402, "考试未开始！"));
            }
            return Mono.zip(
                    findPaperByPaperIdAndCurrentUser(paper.getId()),
                    PaperUtil.getPaperQuestion(configProperties.getUploadPath(paper.getFilePath()), 1 == paper.getUpset()),
                    (answer, questions) -> Result.ok(new PaperAnswerVO(
                            getStatus(answer, paper.getEndTime()),
                            paper.toPaperVO(),
                            answer.getTotalScore(),
                            toPaperQuestionList(questions, paper, answer)
                    ))
            );
        });
    }

    /**
     * 获取试卷状态信息
     * 试卷状态[1未考试,2待考试,3待阅卷,4已通过,5未通过]
     */
    private int getStatus(PaperAnswerPO answer, LocalDateTime endTime) {
        //1.没有提交答案，则返回未考试或待考试
        if (null == answer || !StringUtils.hasText(answer.getAnswer())) {
            return LocalDateTime.now().isAfter(endTime) ? 1 : 2;
        }
        //2.有提交答案，没有总分，则返回待阅卷
        if (null == answer.getTotalScore()) {
            return 3;
        }
        //3.有提交答案，有总分，则返回是否通过
        return 1 == answer.getPassed() ? 4 : 5;
    }

    /**
     * 转换试卷题目
     *
     * @param paperQuestionList
     * @param paper
     * @param answer
     * @return
     */
    private List<PaperQuestionVO> toPaperQuestionList(List<PaperQuestionVO> paperQuestionList, PaperPO paper, PaperAnswerPO answer) {
        //1.判断是否返回答案（条件是允许查看答案并且考试已经结束）
        boolean isLookAnswer = 1 == paper.getLookAnswer() && paper.getEndTime().isBefore(LocalDateTime.now());
        if (!isLookAnswer) {
            //2.如果不允许查看答案，则将答案置空
            paperQuestionList.forEach(question -> question.setAnswer(ApiConst.EMPTY_STRING_ARRAY));
        }

        //3.用户提交的答案
        Map<Integer, String[]> submitAnswerMap = answer.getAnswerMap();
        if (CollectionUtils.isEmpty(submitAnswerMap)) {
            paperQuestionList.forEach(question -> question.setSubmitAnswer(ApiConst.EMPTY_STRING_ARRAY));
        } else {
            paperQuestionList.forEach(question -> question.setSubmitAnswer(submitAnswerMap.get(question.getRowIndex())));
        }

        //4.用户得分
        Map<Integer, Double> questionScoreMap = answer.getQuestionScoreMap();
        if (!CollectionUtils.isEmpty(questionScoreMap)) {
            paperQuestionList.forEach(question -> question.setUserScore(questionScoreMap.get(question.getRowIndex())));
        }
        return paperQuestionList;
    }

    /**
     * 根据试卷ID查询当前用户的试卷答案
     *
     * @param paperId
     * @return
     */
    private Mono<PaperAnswerPO> findPaperByPaperIdAndCurrentUser(Long paperId) {
        return SecurityUtil.getUserId()
                .flatMap(userId -> repository.findPaperByPaperIdAndUserId(paperId, userId))
                .switchIfEmpty(Mono.defer(() -> Mono.just(new PaperAnswerPO())))
                .subscribeOn(Schedulers.parallel());
    }

    @Override
    @Transactional
    public Mono<String> answer(PaperAnswerDTO param) {
        return Mono.zip(
                        SecurityUtil.getUserId().subscribeOn(Schedulers.boundedElastic()),
                        paperService.findEndTimeById(param.getPaperId()).subscribeOn(Schedulers.boundedElastic())
                )
                .transformDeferred(RateLimiterOperator.of(RATE_LIMITER)) // 应用限流
                .flatMap(tuple -> {
                    log.info("开始交卷：{}, param：{}", tuple.getT1(), param);
                    //交卷时间为10分钟，超过10分钟禁止交卷
                    if (LocalDateTime.now().isAfter(tuple.getT2().plusMinutes(10))) {
                        return Mono.error(new CustomException(403, "交卷时间已过！"));
                    }

                    //交卷时间锁10秒
                    final String lockKey = "paper:submit:%d:%d".formatted(param.getPaperId(), tuple.getT1());

                    //获取锁并执行提交
                    return acquireLockAndSubmit(lockKey, () -> submit(param, tuple.getT1()));
                });
    }

    /**
     * 获取锁并执行提交
     *
     * @param lockKey
     * @param operation
     * @return
     */
    private Mono<String> acquireLockAndSubmit(String lockKey, Supplier<Mono<String>> operation) {
        return redisService.lock().lock(lockKey, Duration.ofSeconds(10))
                .flatMap(locked -> {
                    if (!locked) {
                        return Mono.error(new CustomException(429, "系统繁忙，请稍后再试"));
                    }
                    return operation.get()
                            .doOnSuccess(_success -> log.info("交卷成功: {}", lockKey))
                            .doOnError(error -> log.error("交卷失败: {}", lockKey, error))
                            .doFinally(signal -> redisService.lock()
                                    .unlock(lockKey)
                                    .subscribeOn(Schedulers.boundedElastic())
                                    .subscribe(_success -> log.info("交卷释放锁成功: {}", lockKey),
                                            error -> log.error("交卷释放锁失败: {}", lockKey, error)));
                })
                .onErrorResume(error -> {
                    log.error("获取锁异常: {}", lockKey, error);
                    return Mono.error(new CustomException(500, "系统异常，请稍后再试"));
                });
    }

    /**
     * 提交试卷
     *
     * @param param
     * @param userId
     * @return
     */
    private Mono<String> submit(PaperAnswerDTO param, Long userId) {
        return repository.existsByPaperIdAndUserId(param.getPaperId(), userId)
                .flatMap(exists -> {
                    if (exists) {
                        log.warn("重复交卷: userId={}, paperId={}", userId, param.getPaperId());
                        return Mono.error(new CustomException(403, "已交卷，无法重复提交！"));
                    }

                    //重试的插入操作
                    return Mono.defer(() -> template.insert(new PaperAnswerPO()
                                    .setAnswer(JSON.toJSONString(param.getAnswer()))
                                    .setUserId(userId)
                                    .setPaperId(param.getPaperId())))
                            .retryWhen(RETRY_SPEC)
                            .thenReturn("已交卷！");
                });
    }

    @Override
    public Mono<PaperAnswerPO> findAnswerByPaperId(Long paperId, Long answerId) {
        return repository.findAnswerByPaperId(paperId, answerId);
    }

    @Override
    public Mono<Boolean> hasNextPaperAnswerByPaperId(Long paperId) {
        return repository.findCountByPaperId(paperId).map(count -> count > 1);
    }

    @Override
    public Mono<String> update(Long id, String questionScore, Double totalScore, Integer passed) {
        return template.update(PaperAnswerPO.class)
                .matching(Query.query(Criteria.where(this.primaryKey).is(id)))
                .apply(Update.update("question_score", questionScore)
                        .set("total_score", totalScore)
                        .set("passed", passed))
                .thenReturn("阅卷完成！")
                .onErrorResume(err -> Mono.error(new CustomException(ERROR_402)))
                .as(transactionalOperator::transactional);
    }

    @Override
    public Mono<List<PaperAnswerPO>> findAnswerListByPaperId(Long paperId) {
        return repository.findPaperAnswerByPaperId(paperId).collectList();
    }
}
